/*
 * Created by Jaren at 2022/1/28 9:05
 */
import 'dart:async';
import 'dart:core';
import 'dart:io';
import 'dart:typed_data';

import 'package:inmotion_protocol/base/base_message.dart';
import 'package:inmotion_protocol/base/file/file_manager.dart';
import 'package:inmotion_protocol/base/protocol.dart';
import 'package:inmotion_protocol/common/exceptions.dart';
import 'package:inmotion_protocol/ezcan/ezcan_message.dart';
import 'package:inmotion_protocol/ezcan/file/response.dart';
import 'package:inmotion_protocol/ezcan/util.dart';
import 'package:inmotion_protocol/util/byte_util.dart';
import 'package:inmotion_protocol/util/file.dart';
import 'package:inmotion_protocol/util/log.dart';

class EzCANFileManager extends IFileManager {
  static const _tag = "EzCANFileManager";

  // 创建/打开文件，参数为tag（用来区分文件，使用文件检验和的值, 1byte）和文件类型，包括升级文件（0）、音乐文件（1）、灯效文件（2），回复消息内容为当前文件大小，0为新创建的文件，>0为打开已经存在的文件，<0为创建/打开失败，可根据回复消息进行断点续传
  // 设置文件写入位置（用于断点续传）
  // 发送文件数据（接收回复，回复数据中的data前4字节为错误码，1为成功，其余为失败）
  // 重发（发送失败时，回复超时或回复的为失败，回复超时时应重设offset为上次成功的位置，重发当前包；回复的为失败时应重试5次，若失败则文件上传失败，并给出错误码）
  // 关闭文件

  EzcanConfig _config;

  // 上次发送成功的offset, 可用作断点续传
  int lastValidOffset = 0;

  int retryTimes = 0;
  int writeFileRetryTimes = 0;

  int existsFileSize = 0;

  EzCANFileManager(Protocol<IMessage> protocol, this._config) : super(protocol);

  @override
  void init(FileConfig config) {
    reset();
    if (config is! EzcanConfig) {
      throw InvalidParamException('Config should be an instance of EzcanConfig.');
    }
    _config = config;
    LogUtils.d(_tag, "init with config " + config.toString());
  }

  @override
  Stream<FileEvent> start() {
    onStarted();

    StreamController<FileEvent> streamController = StreamController(sync: true);

    LogUtils.d("ezcan file manager", "existsFileSize: " + existsFileSize.toString());
    _openFile().then((existsFileSize) {
      streamController.add(FileEvent(FileEvent.fileOpened));
      if (existsFileSize <= _config.localFile.lengthSync()) {
        this.existsFileSize = existsFileSize;
      } else {
        this.existsFileSize = 0;
      }
      // 设置写文件偏移地址
      return _setFileOffset(_config.type, this.existsFileSize);
    }).catchError((error) {
      if (error is CanceledError || error is FileOperationStepError) {
        streamController.addError(error);
      }
      return Future.value(false);
    }).then((shouldContinue) {
      if (!shouldContinue) {
        return Future.value(false);
      }
      // 写文件
      return _writeFile(_config.type, existsFileSize, _config.localFile, streamController);
    }).catchError((error) {
      if (error is CanceledError || error is FileOperationStepError) {
        streamController.addError(error);
      }
      return Future.value(false);
    }).then((shouldContinue) {
      if (!shouldContinue) {
        return Future.value(false);
      }
      // 关闭文件
      return _closeFile();
    }).catchError((ignore) {
      streamController.add(FileEvent.byFileClosed());
      return Future.value(false);
    }).then((shouldContinue) {
      if (!shouldContinue) {
        return Future.value(false);
      }
      streamController.add(FileEvent.byFileClosed());
      return Future.value(true);
    }).catchError((ignore) {
      return Future.value(false);
    });
    return streamController.stream;
  }

  @override
  void reset() {
    super.reset();
    retryTimes = 0;
    lastValidOffset = 0;
  }

  Future<int> _openFile() {
    if (checkInterrupted()) throw CanceledError();

    return protocol.openFile(type: _config.type, tag: _config.tag).then((rsp) {
      if (rsp.isSuccess()) {
        retryTimes = 0;
        return Future.value((rsp as OpenFileResponse).existsFileSize);
      }
      throw FileOperationStepError(IFileManager.stepOpenFile, rsp.originError);
    });
  }

  Future<bool> _setFileOffset(int type, int offset) {
    if (checkInterrupted()) throw CanceledError();

    return protocol.setFileOffset(type: type, offset: offset).then((rsp) {
      if (rsp.isSuccess()) {
        retryTimes = 0;
        return Future.value(true);
      }
      throw FileOperationStepError(IFileManager.stepSetFileOffset, rsp.originError);
    });
  }

  Future<bool> _writeFile(int type, int initOffset, File file, StreamController progressTicker) {
    if (checkInterrupted()) throw CanceledError();
    return file.readAsBytes().then((fileBytes) {
      return _writeFileImpl(type, initOffset, fileBytes, progressTicker);
    });
  }

  Future<bool> _writeFileImpl(int type, int initOffset, Uint8List fileBytes, StreamController progressTicker) {
    if (checkInterrupted()) throw CanceledError();
    int totalSize = fileBytes.length;
    Uint8ClampedList pkg = _subNextPkgBytes(fileBytes, initOffset, totalSize, sizePeerPkg());
    if (pkg.isEmpty) {
      return Future.value(true);
    }
    return _writeFileImpl0(type, initOffset, pkg).then((none) {
      /// trigger
      progressTicker.add(FileProgressEvent(initOffset + pkg.length, totalSize, aveSpeed, rtSpeed, costSeconds, remainSeconds));
      onTransferring(initOffset, pkg.length, totalSize);

      int nextOffset = initOffset + pkg.length;
      if (_nextPkgSize(nextOffset, totalSize, sizePeerPkg()) <= 0) {
        return Future.value(true);
      }
      return _writeFileImpl(type, nextOffset, fileBytes, progressTicker);
    });
  }

  Future<bool> _writeFileImpl0(int type, int offset, Uint8ClampedList data) {
    return protocol.writeFile(data, offset: offset, contentLength: data.length).then((rsp) {
      if (rsp.isSuccess()) {
        writeFileRetryTimes = 0;
        return Future.value(true);
      }
      throw FileOperationStepError(IFileManager.stepTransferFile, rsp.originError);
    });
  }

  Uint8ClampedList _subNextPkgBytes(List<int> fileBytes, int offset, int total, int sizePeerPkg) {
    int pkgSize = _nextPkgSize(offset, total, sizePeerPkg);
    return Uint8ClampedList.fromList(fileBytes.sublist(offset, offset + pkgSize));
  }

  int _nextPkgSize(int offset, int total, int sizePeerPkg) {
    int nextPkgSize = total - offset;
    if (nextPkgSize >= sizePeerPkg) nextPkgSize = sizePeerPkg;
    return nextPkgSize;
  }

  Future<bool> _closeFile() {
    return protocol.closeFile().then((rsp) {
      return EzcanUtil.parseSettingsRsp(rsp as EzcanMessage).isSuccess();
    });
  }

  @override
  void destroy() {
    interrupt();
  }

  @override
  int sizePeerPkg() {
    return 150;
  }
}

class EzcanConfig extends FileConfig {
  static const int typeFirmware = 0;
  static const int typeSound = 1;
  static const int typeLightEffect = 2;

  int? tag;
  int type;

  EzcanConfig(File localFile, this.type) {
    this.localFile = localFile;
    tag = genFileTag(type, localFile);
  }

  EzcanConfig.withTag(File localFile, this.type, this.tag) {
    this.localFile = localFile;
  }

  static EzcanConfig byFwUpdate(File localFile) {
    var config = EzcanConfig(localFile, typeFirmware);
    config.tag = genFileTag(typeFirmware, localFile);
    return config;
  }

  static EzcanConfig bySoundPack(File localFile, int soundPackId, [bool addIdToFileEnd = true]) {
    if (addIdToFileEnd) {
      Uint8ClampedList idBytes = ByteUtil.num2BytesLe(soundPackId, 4);
      int fileSize = localFile.lengthSync();
      int sum = EzCANFileUtils.calculateFileSum(localFile, 0, fileSize - 5, idBytes);
      FileUtils.rewriteFile(localFile, fileSize - 5, ByteUtil.buildBytes(idBytes, Uint8ClampedList.fromList([sum])));
    }
    var config = EzcanConfig(localFile, typeSound);
    config.tag = genFileTag(typeSound, localFile);
    return config;
  }

  static EzcanConfig byLightEffect(File localFile) {
    var config = EzcanConfig(localFile, typeLightEffect);
    config.tag = genFileTag(typeLightEffect, localFile);
    return config;
  }

  /// gen file sum check as tag
  static int genFileTag(int type, File file) {
    return type == typeFirmware ? EzCANFileUtils.calculateUpdateFileSum(file) : EzCANFileUtils.calculateFileSum(file);
  }

  @override
  String toString() {
    return "EzcanConfig{localFile: " + (localFile.toString()) + ", type: " + type.toString() + ", tag: " + tag.toString() + "}";
  }
}
